안녕하세요, 김도현입니다.
토이 프로젝트를 진행하는 과정에서, 내부망 안에 있는 홈서버를 외부에서 사용해야 하는 상황이 생겼습니다.
이를 해결하며 사용한 WireGuard 가 무엇이고, 어떻게 동작하며, 어떤 점을 조심해야 하는지까지 이야기해보려 합니다.
WireGuard가 필요했던 이유
이 프로젝트에는 무거운 연산을 담당하는 AI 워커가 있습니다.
이 작업은 CPU로 돌리면 수 분씩 걸릴 만큼 무거워서 GPU가 거의 필수인데, 클라우드에서 GPU 인스턴스를 상시 켜두는 건 비용이 만만치 않습니다.
그래서 기숙사에 있는 GPU가 달린 데스크탑을 홈서버로 띄워서 AI 워커를 돌리기로 했습니다.
백엔드(NestJS)는 클라우드에, AI 워커는 데스크탑에 두고, 백엔드가 요청을 이 서버로 보내는 구조를 그렸죠.
[클라우드 백엔드] ---- 작업 요청 ---> [AI 워커(GPU)]
그런데 여기서 문제가 있습니다.
지스트 내부망은 방화벽 뒤에 있어서, 외부에서 들어오는 연결(inbound)을 받을 수 없습니다.
즉 클라우드 백엔드가 내부망의 AI 워커에게 직접 요청을 보낼 방법이 없는 것이죠.
포트포워딩이나 인바운드 개방은 개인이 건드릴 수 있는 영역도 아니고, 내부망 정책상 애초에 허용되지도 않습니다.
따라서 밖으로 나가는(outbound) 연결만으로 두 서버를 하나의 사설 네트워크처럼 묶어주는 방법이 필요했습니다.
WireGuard?
WireGuard는 리눅스 커널에 내장된 모던 VPN 프로토콜입니다.
기존 VPN(OpenVPN, IPsec 등)이 설정이 복잡하고 무거웠던 것과 달리, WireGuard는 다음과 같은 특징을 가집니다.
- 간결함: 코드베이스가 수천 줄 수준으로 작아 감사(audit)가 쉽고 버그 표면이 작습니다.
- 빠름: 커널에서 동작하고 최신 암호(Curve25519, ChaCha20 등)를 사용해 성능이 뛰어납니다.
- 키 기반 인증: 아이디/비밀번호가 아니라 SSH처럼 공개키/개인키 쌍으로 서로를 인증합니다.
핵심은 WireGuard가 클라이언트-서버라는 개념 대신 peer 라는 대등한 단위로 동작한다는 점입니다.
각 peer는 자기만의 키 쌍을 갖고, 상대 peer의 공개키를 등록해 서로 신뢰합니다.
어떻게 동작할까?
WireGuard의 동작은 크게 세 가지 개념으로 이해할 수 있습니다.
키 쌍 (Public / Private Key)
각 peer는 등록 시 무작위로 개인키와 공개키를 만듭니다.
개인키는 자기 기기에만 보관하고, 공개키는 상대 peer에게 알려줍니다.
그러면 각 peer는 자기 개인키 + 상대 공개키를 조합해, 둘만 계산해낼 수 있는 동일한 세션 키를 각자 따로 만들어냅니다. (Diffie–Hellman 키 교환)
실제 데이터는 이 세션 키로 암호화되기 때문에, 비밀번호는 물론 세션 키조차 네트워크로 주고받지 않습니다.
개인키만 새어나가지 않으면 중간에서 패킷을 가로채도 세션 키를 만들 수 없어 내용을 알 수 없습니다.
AllowedIPs (누구의 트래픽을 이 터널로 보낼까)
WireGuard는 각 peer에 대해 AllowedIPs 라는 사설 IP 대역을 지정합니다.
예를 들어 클라우드 백엔드를
10.0.0.1, 데스크탑 AI 워커를 10.0.0.2 로 묶어두면,백엔드가
10.0.0.2 로 보낸 패킷은 자동으로 WireGuard 터널을 통해 AI 워커에게 전달됩니다.즉, 물리적으로는 인터넷 너머에 떨어져 있어도 논리적으로는 같은 사설망처럼 다룰 수 있습니다.
Handshake (UDP)
WireGuard는 UDP(기본 51820번 포트) 위에서 동작합니다.
연결을 시작하는 peer가 상대에게 handshake 패킷을 보내면, 서로의 공개키로 세션 키를 교환하고 암호화된 터널이 열립니다.
연결이 성립하고 나면 그 위로는 그냥 평범한 IP 패킷이 오갑니다.
방화벽은 어떻게 넘을까?
여기가 우리 문제의 핵심입니다.
데스크탑 AI 워커는 인바운드는 못 받지만 아웃바운드(밖으로 나가는 연결)는 가능합니다.
그래서 발상을 뒤집습니다.
클라우드 백엔드가 데스크탑 워커에 접속하는 게 아니라, 데스크탑 워커가 공인 IP를 가진 클라우드 쪽으로 먼저 WireGuard 터널을 겁니다.
여기서 "어차피 방화벽이 막는데, 방향만 바꾼다고 뭐가 달라지지?" 라는 의문이 들 수 있습니다.
핵심은 방화벽이 패킷을 '방향'만 보고 막는 게 아니라, 오가는 '연결(connection)'을 통째로 기억하며 판단한다는 점입니다. (이를 stateful, 연결 추적이라고 합니다.)
방화벽의 규칙은 대략 이렇습니다.
- 밖에서 안으로 새로 시작되는 연결 → 차단
- 안에서 밖으로 내가 먼저 시작한 연결과, 그에 대한 응답 → 허용
사실 이건 당연한 동작입니다.
우리가 브라우저로 구글에 접속할 때, 요청은 밖으로 나가지만 구글의 응답 패킷은 밖에서 안으로 들어옵니다.
이 응답까지 막아버리면 서비스 사용 자체가 불가능하겠죠.
그래서 방화벽은 "내가 먼저 시작한 연결의 답장"만큼은 안으로 들여보내 줍니다.
전화에 비유하면 이렇습니다.
- 데스크탑은 밖에서 걸려오는 전화는 못 받습니다. (inbound 차단)
- 하지만 내가 먼저 전화를 걸면, 그 통화가 연결된 동안에는 양쪽이 자유롭게 대화할 수 있습니다.
WireGuard도 똑같습니다.
데스크탑 워커가 클라우드로 먼저 "전화를 걸어" 터널을 열면, 방화벽은 이 outbound 연결을 허용 목록에 올려두고 되돌아오는 트래픽을 위한 길을 열어 둡니다.
그리고 WireGuard 터널은 한 번 열리면 그 위로 양방향 통신이 되기 때문에, 이제 클라우드 백엔드가 보내는 요청도 이미 열려있는 이 연결의 '답장'인 것처럼 방화벽을 통과해 데스크탑 워커에 도달합니다.
즉, 대화를 먼저 트는 건 안쪽(데스크탑)이지만, 통로가 뚫린 뒤로는 바깥(백엔드)에서도 얼마든지 먼저 말을 걸 수 있게 되는 것이죠.
여기서 중요한 설정이
PersistentKeepalive 입니다.앞의 전화 비유를 이어가면, 통화가 너무 오래 조용하면 교환기가 "끊어졌나 보다" 하고 회선을 정리하는 것과 같습니다.
방화벽도 일정 시간 트래픽이 없으면 아까 열어둔 연결을 잊어버리고, 그 순간 백엔드가 보낸 요청은 다시 막혀버립니다.
그래서
PersistentKeepalive = 25 처럼 설정해두면 25초마다 작은 패킷을 흘려보내 "나 아직 통화 중이야" 하고 연결을 계속 살려둡니다.덕분에 백엔드가 언제 요청을 보내도 터널이 살아있어 워커에게 도달합니다.
토이 프로젝트에서는 이렇게 연결했다
전체 그림은 이렇습니다.
- 클라우드 백엔드 서버(공인 IP)에 WireGuard peer를 띄우고
10.0.0.1을 할당합니다.
- 데스크탑 AI 워커에 WireGuard peer를 띄우고
10.0.0.2를 할당한 뒤,Endpoint를 클라우드 서버로 지정합니다.
- 데스크탑 워커가 밖으로 터널을 열고
PersistentKeepalive로 유지하면, 백엔드는http://10.0.0.2:8000처럼 워커를 바로 옆 서버처럼 호출할 수 있습니다.
데스크탑 워커 쪽 설정은 대략 이런 모습입니다.
[Interface] PrivateKey = <데스크탑 워커 개인키> Address = 10.0.0.2/24 [Peer] PublicKey = <클라우드 백엔드 공개키> Endpoint = example.com:51820 # 공인 IP를 가진 클라우드 AllowedIPs = 10.0.0.1/32 PersistentKeepalive = 25 # 방화벽 구멍 유지
sequenceDiagram participant API as 클라우드 백엔드 (10.0.0.1) participant WG as WireGuard 터널 participant AI as 데스크탑 AI 워커 (10.0.0.2) AI ->> WG: outbound handshake (UDP 51820) WG ->> API: 터널 수립 Note over API,AI: 이후 양방향 사설망처럼 동작 (PersistentKeepalive로 유지) API ->> WG: http://10.0.0.2:8000 로 작업 요청 WG ->> AI: 터널 통해 전달 AI ->> AI: AI 모델 실행 AI ->> WG: 결과 반환 WG ->> API: 백엔드로 전달
방화벽 입장에서는 데스크탑 워커가 밖으로 연 평범한 UDP 연결 하나로만 보입니다.
하지만 그 위에서는 두 서버가 마치 같은 사설망에 있는 것처럼 자유롭게 통신하고 있는 것이죠.
WireGuard는 처음부터 이런 용도로 설계된 정식 VPN이라 빠르고 안정적이며 암호화까지 보장됩니다.
간단하게 시도해보기: Tailscale
여기까지 직접 해보면, 키를 만들고 config를 짜고 공인 IP를 가진 클라우드 서버까지 마련하는 게 은근히 번거롭습니다.
이 과정을 간단하게 처리해주는 도구가 Tailscale입니다.
Tailscale은 WireGuard를 그대로 엔진으로 쓰면서, 귀찮은 부분을 대신 맡아줍니다.
- 키 교환·관리: 계정으로 로그인만 하면 각 기기의 키 등록과 공유를 알아서 해줍니다.
- NAT 통과: 중계 서버(DERP)와 NAT 홀펀칭을 자동으로 시도해, 공인 IP 서버를 직접 두지 않아도 방화벽 뒤 기기끼리 연결됩니다.
- 주소 할당: 각 기기에 고정 사설 IP를 자동으로 부여해, 위에서 손으로 정한
10.0.0.x를 신경 쓸 필요가 없습니다.
즉, "데스크탑에 Tailscale 깔고, 클라우드에도 깔고, 같은 계정으로 로그인" 만으로 앞에서 한 일이 거의 끝납니다.
하지만 편해진 만큼, 원리를 알고 나면 마냥 안심할 수만은 없는 지점도 있습니다.
Tailscale은 두 기기를 직접(UDP 홀펀칭) 연결하려 하지만, 직접 연결이 안 되면 DERP라는 Tailscale의 중계 서버를 거쳐 트래픽을 흘려보냅니다.
이때도 패킷은 두 기기의 키로 끝단 암호화(E2E)되어 있어 중계 서버가 내용을 열어보진 못하지만, 그렇다고 "완전히 안전하다"고 단정하긴 어렵습니다.
결국 Tailscale을 쓴다는 건 내 사설망의 키 배포·접근 제어·연결 중개를 남의 서비스에 맡긴다는 뜻입니다.
그 중앙 서버나 계정이 뚫리거나, 연결 메타데이터가 노출되거나, 서비스 정책·장애에 내 네트워크가 휘둘리는 등 - 꼭 위에서 말한 경로가 아니더라도 내가 미처 생각 못 한 곳에서 위험이 생길 수 있습니다.
그래서 다루는 데이터가 민감하고 통제권을 완전히 쥐고 싶다면, 앞서처럼 WireGuard를 직접 구성하는 것을 고려할 수 있습니다.
그래도 조심할 것
WireGuard는 편리하지만, 결국 이 터널은 내부망 안으로 들어오는 문을 하나 뚫는 행위이기도 합니다.
- 개인키 관리: 개인키가 유출되면 그 peer 행세를 하며 내부망에 접근할 수 있습니다. 키 파일 권한(
600)과 보관에 주의해야 합니다.
- AllowedIPs 최소화: 필요한 IP/포트만 열어야 합니다. 넓게 열면 터널이 내부망 전체로의 통로가 될 수 있습니다.
내가 편하려고 뚫은 터널은 관리가 소홀하면 그대로 공격자의 통로가 될 수 있습니다.
그래서 VPN 하나를 띄우더라도 키·접근 범위 등을 확인해야 합니다.